﻿// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/LICENSE

// Define search commands. Depends on dialog.js or another
// implementation of the openDialog method.

// Replace works a little oddly -- it will do the replace on the next
// Ctrl-G (or whatever is bound to findNext) press. You prevent a
// replace by making sure the match is no longer selected when hitting
// Ctrl-G.

(function (mod) {
  if (typeof exports == "object" && typeof module == "object")
    // CommonJS
    mod(
      require("../../lib/codemirror"),
      require("./searchcursor"),
      require("../dialog/dialog")
    );
  else if (typeof define == "function" && define.amd)
    // AMD
    define(["../../lib/codemirror", "./searchcursor", "../dialog/dialog"], mod);
  // Plain browser env
  else mod(CodeMirror);
})(function (CodeMirror) {
  "use strict";

  function searchOverlay(query, caseInsensitive) {
    if (typeof query == "string")
      query = new RegExp(
        query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"),
        caseInsensitive ? "gi" : "g"
      );
    else if (!query.global)
      query = new RegExp(query.source, query.ignoreCase ? "gi" : "g");

    return {
      token: function (stream) {
        query.lastIndex = stream.pos;
        var match = query.exec(stream.string);
        if (match && match.index == stream.pos) {
          stream.pos += match[0].length || 1;
          return "searching";
        } else if (match) {
          stream.pos = match.index;
        } else {
          stream.skipToEnd();
        }
      },
    };
  }

  function SearchState() {
    this.posFrom = this.posTo = this.lastQuery = this.query = null;
    this.overlay = null;
  }

  function getSearchState(cm) {
    return cm.state.search || (cm.state.search = new SearchState());
  }

  function queryCaseInsensitive(query) {
    return typeof query == "string" && query == query.toLowerCase();
  }

  function getSearchCursor(cm, query, pos) {
    // Heuristic: if the query string is all lowercase, do a case insensitive search.
    return cm.getSearchCursor(query, pos, {
      caseFold: queryCaseInsensitive(query),
      multiline: true,
    });
  }

  function persistentDialog(cm, text, deflt, onEnter, onKeyDown) {
    cm.openDialog(text, onEnter, {
      value: deflt,
      selectValueOnOpen: true,
      closeOnEnter: false,
      closeOnBlur: false,
      onClose: function () {
        clearSearch(cm);
      },
      onKeyDown: onKeyDown,
    });
  }

  function dialog(cm, text, shortText, deflt, f) {
    if (cm.openDialog)
      cm.openDialog(text, f, {
        value: deflt,
        selectValueOnOpen: true,
        closeOnBlur: false,
      });
    else f(prompt(shortText, deflt));
  }

  function confirmDialog(cm, text, shortText, fs) {
    if (cm.openConfirm) cm.openConfirm(text, fs);
    else if (confirm(shortText)) fs[0]();
  }

  function parseString(string) {
    return string.replace(/\\([nrt\\])/g, function (match, ch) {
      if (ch == "n") return "\n";
      if (ch == "r") return "\r";
      if (ch == "t") return "\t";
      if (ch == "\\") return "\\";
      return match;
    });
  }

  function parseQuery(query) {
    var isRE = query.match(/^\/(.*)\/([a-z]*)$/);
    if (isRE) {
      try {
        query = new RegExp(isRE[1], isRE[2].indexOf("i") == -1 ? "" : "i");
      } catch (e) {} // Not a regular expression after all, do a string search
    } else {
      query = parseString(query);
    }
    if (typeof query == "string" ? query == "" : query.test("")) query = /x^/;
    return query;
  }

  function startSearch(cm, state, query) {
    state.queryText = query;
    state.query = parseQuery(query);
    cm.removeOverlay(state.overlay, queryCaseInsensitive(state.query));
    state.overlay = searchOverlay(
      state.query,
      queryCaseInsensitive(state.query)
    );
    cm.addOverlay(state.overlay);

    if (cm.showMatchesOnScrollbar) {
      if (state.annotate) {
        state.annotate.clear();
        state.annotate = null;
      }
      state.annotate = cm.showMatchesOnScrollbar(
        state.query,
        queryCaseInsensitive(state.query)
      );
    }
    showToolBar();
  }

  function doSearch(cm, rev, persistent, immediate) {
    var state = getSearchState(cm);
    if (state.query) return findNext(cm, rev);
    var q = cm.getSelection() || state.lastQuery;
    if (q instanceof RegExp && q.source == "x^") q = null;
    if (persistent && cm.openDialog) {
      var hiding = null;
      var searchNext = function (query, event) {
        CodeMirror.e_stop(event);
        if (!query) return;
        if (query != state.queryText) {
          startSearch(cm, state, query);
          state.posFrom = state.posTo = cm.getCursor();
        }

        if (hiding) hiding.style.opacity = 1;
        findNext(cm, event.shiftKey, function (_, to) {
          var dialog;
          if (
            to.line < 3 &&
            document.querySelector &&
            (dialog = cm.display.wrapper.querySelector(".CodeMirror-dialog")) &&
            dialog.getBoundingClientRect().bottom - 4 >
              cm.cursorCoords(to, "window").top
          )
            (hiding = dialog).style.opacity = 0.4;
        });
      };
      persistentDialog(cm, getQueryDialog(cm), q, searchNext, function (
        event,
        query
      ) {
        var keyName = CodeMirror.keyName(event);
        var extra = cm.getOption("extraKeys"),
          cmd =
            (extra && extra[keyName]) ||
            CodeMirror.keyMap[cm.getOption("keyMap")][keyName];
        if (
          cmd == "findNext" ||
          cmd == "findPrev" ||
          cmd == "findPersistentNext" ||
          cmd == "findPersistentPrev"
        ) {
          CodeMirror.e_stop(event);
          startSearch(cm, getSearchState(cm), query);
          cm.execCommand(cmd);
        } else if (cmd == "find" || cmd == "findPersistent") {
          CodeMirror.e_stop(event);
          searchNext(query, event);
        }
      });
      if (immediate && q) {
        startSearch(cm, state, q);
        findNext(cm, rev);
      }
    } else {
      dialog(cm, getQueryDialog(cm), "Search for:", q, function (query) {
        if (query && !state.query)
          cm.operation(function () {
            startSearch(cm, state, query);
            state.posFrom = state.posTo = cm.getCursor();
            findNext(cm, rev);
          });
      });
    }
    hideToolBar();
  }

  function findNext(cm, rev, callback) {
    cm.operation(function () {
      var state = getSearchState(cm);
      var cursor = getSearchCursor(
        cm,
        state.query,
        rev ? state.posFrom : state.posTo
      );
      if (!cursor.find(rev)) {
        cursor = getSearchCursor(
          cm,
          state.query,
          rev
            ? CodeMirror.Pos(cm.lastLine())
            : CodeMirror.Pos(cm.firstLine(), 0)
        );
        if (!cursor.find(rev)) return;
      }
      cm.setSelection(cursor.from(), cursor.to());
      cm.scrollIntoView({ from: cursor.from(), to: cursor.to() }, 20);
      state.posFrom = cursor.from();
      state.posTo = cursor.to();
      if (callback) callback(cursor.from(), cursor.to());
    });
  }

  function clearSearch(cm) {
    cm.operation(function () {
      var state = getSearchState(cm);
      state.lastQuery = state.query;
      if (!state.query) return;
      state.query = state.queryText = null;
      cm.removeOverlay(state.overlay);
      if (state.annotate) {
        state.annotate.clear();
        state.annotate = null;
      }
    });
  }

  function getQueryDialog(cm) {
    return (
      '<span class="CodeMirror-search-label">' +
      SEARCH +
      '</span> <input type="text" style="width: 10em" class="CodeMirror-search-field"/> <span style="color: #888" class="CodeMirror-search-hint">' +
      SEARCHTIP +
      '</span><span id="dia_close"'
    );
  }
  function getReplaceQueryDialog(cm) {
    return (
      '<input type="text" style="width: 10em" class="CodeMirror-search-field"/> <span style="color: #888" class="CodeMirror-search-hint">' +
      SEARCHTIP +
      "</span>"
    );
  }
  function getReplacementQueryDialog(cm) {
    return (
      '<span class="CodeMirror-search-label">' +
      RWITH +
      '</span> <input type="text" style="width: 10em" class="CodeMirror-search-field"/>'
    );
  }
  function getDoReplaceConfirm(cm) {
    return '<button><i class="fad fa-check"></i></button> <button><i class="fad fa-level-down"></i></button> <button><i class="fad fa-check-double"></i></button> <button><i class="fad fa-times"></i></button>';
  }

  function replaceAll(cm, query, text) {
    var state = getSearchState(cm);
    cm.removeOverlay(state.overlay, queryCaseInsensitive(query));
    if (cm.showMatchesOnScrollbar) {
      if (state.annotate) {
        state.annotate.clear();
        state.annotate = null;
      }
    }
    cm.operation(function () {
      for (var cursor = getSearchCursor(cm, query); cursor.findNext(); ) {
        if (typeof query != "string") {
          var match = cm.getRange(cursor.from(), cursor.to()).match(query);
          cursor.replace(
            text.replace(/\$(\d)/g, function (_, i) {
              return match[i];
            })
          );
        } else cursor.replace(text);
      }
    });
  }
  function replace(cm, all) {
    hideToolBar();

    if (cm.getOption("readOnly")) return;
    var query = cm.getSelection() || getSearchState(cm).lastQuery;

    var dialogText =
      '<span class="CodeMirror-search-label">' +
      (all ? REPLACE + ALL : REPLACE) +
      " </span>";
    dialog(
      cm,
      dialogText + getReplaceQueryDialog(cm),
      dialogText,
      query,
      function (query) {
        if (!query) return;
        query = parseQuery(query);
        dialog(
          cm,
          getReplacementQueryDialog(cm),
          cm.phrase("Replace with:"),
          "",
          function (text) {
            text = parseString(text);
            if (all) {
              replaceAll(cm, query, text);
            } else {
              clearSearch(cm);
              var state = getSearchState(cm);
              cm.removeOverlay(state.overlay, queryCaseInsensitive(query));
              state.overlay = searchOverlay(query, queryCaseInsensitive(query));
              cm.addOverlay(state.overlay);
              if (cm.showMatchesOnScrollbar) {
                if (state.annotate) {
                  state.annotate.clear();
                  state.annotate = null;
                }
                state.annotate = cm.showMatchesOnScrollbar(
                  query,
                  queryCaseInsensitive(query)
                );
              }
              cm.focus();
              var cursor = getSearchCursor(cm, query, cm.getCursor("from"));
              var advance = function () {
                var start = cursor.from(),
                  match;
                if (!(match = cursor.findNext())) {
                  cursor = getSearchCursor(cm, query);
                  if (
                    !(match = cursor.findNext()) ||
                    (start &&
                      cursor.from().line == start.line &&
                      cursor.from().ch == start.ch)
                  )
                    return;
                }
                cm.setSelection(cursor.from(), cursor.to());
                cm.scrollIntoView({ from: cursor.from(), to: cursor.to() });
                confirmDialog(
                  cm,
                  getDoReplaceConfirm(cm),
                  cm.phrase("Replace?"),
                  [
                    function () {
                      doReplace(match);
                    },
                    advance,
                    function () {
                      replaceAll(cm, query, text);
                    },
                    function () {
                      cm.removeOverlay(
                        state.overlay,
                        queryCaseInsensitive(query)
                      );

                      if (cm.showMatchesOnScrollbar) {
                        if (state.annotate) {
                          state.annotate.clear();
                          state.annotate = null;
                        }
                      }
                    },
                  ]
                );
              };
              var doReplace = function (match) {
                cursor.replace(
                  typeof query == "string"
                    ? text
                    : text.replace(/\$(\d)/g, function (_, i) {
                        return match[i];
                      })
                );
                advance();
              };
              advance();
            }
          }
        );
      }
    );
  }

  CodeMirror.commands.find = function (cm) {
    clearSearch(cm);
    hideToolBar();
    doSearch(cm);
  };
  CodeMirror.commands.findPersistent = function (cm) {
    clearSearch(cm);
    doSearch(cm, false, true);
  };
  CodeMirror.commands.findPersistentNext = function (cm) {
    doSearch(cm, false, true, true);
  };
  CodeMirror.commands.findPersistentPrev = function (cm) {
    doSearch(cm, true, true, true);
  };
  CodeMirror.commands.findNext = doSearch;
  CodeMirror.commands.findPrev = function (cm) {
    doSearch(cm, true);
  };
  CodeMirror.commands.clearSearch = clearSearch;
  CodeMirror.commands.replace = replace;
  CodeMirror.commands.replaceAll = function (cm) {
    replace(cm, true);
  };
});
